M2.855 · Modelos avanzados de minería de datos · PEC3
2020-2 · Máster universitario en Ciencia de datos (Data science)
Estudios de Informática, Multimedia y Telecomunicación
En esta práctica veremos diferentes métodos supervisados y trataremos de optimizar diferentes métricas. Veremos como los diferentes modelos clasifican los puntos y con cuales obtenemos mayor precisión. Después aplicaremos todo lo que hemos aprendido hasta ahora a un dataset nuevo simulando un caso práctico real.
Importante: Cada uno de los ejercicios puede suponer varios minutos de ejecución, por lo que la entrega se hará en formato notebook y en formato html donde se vea el código y los resultados, junto con los comentarios de cada ejercicio. Para exportar el notebook a html se puede hacer desde el menú File $\to$ Download as $\to$ HTML.
from scipy import stats
import random
import numpy as np
import plotly.express as px
import plotly.graph_objs as go
import plotly as py
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import tree
from sklearn import datasets
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split, GridSearchCV,cross_val_score
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import OneHotEncoder
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.utils import check_random_state
import numpy.random as nr
# Visualizar árboles
from IPython.display import Image
import pydotplus
from six import StringIO
# UMAP para la reducción de dimensionalidad
import umap
# Visualización
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
from scipy import stats
import random
import plotly.express as px
import plotly.graph_objs as go
import plotly as py
import matplotlib.image as mpimg
%matplotlib inline
En la PEC anterior trabajamos con el dataset MNIST, compuesto de miles de dígitos manuscritos del 0 al 9. Donde cada imagen se compone de 784 píxeles (imágenes de 28 x 28). Redujimos a dos dimensiones el dataset utilizando diferentes técnicas de clustering, y vimos que con UMAP conseguíamos separar las clases (dígitos) bastante bien en 2D.
En este primer ejercicio iremos un paso más allá, utilizaremos diferentes métodos supervisados para predecir las diferentes clases. El objetivo será que, dada una imagen nueva, el algoritmo sea capaz de clasificar correctamente el número de la imagen.
Empezamos cargando el dataset y visualizando un ejemplo de cada dígito.
X, y = datasets.fetch_openml('mnist_784', version=1, return_X_y=True, as_frame=False)
random_state = check_random_state(0)
permutation = random_state.permutation(X.shape[0])
X = X[permutation]
y = y[permutation].astype(int)
X = X.reshape((X.shape[0], -1))
fig, axis = plt.subplots(1, 10, figsize=(12, 6))
for i, ax in zip(range(10), axis):
ax.imshow(X[y == i][0].reshape(28, 28), cmap='gray_r')
ax.set_title(str(i))
ax.axis('off')
plt.tight_layout()
Este dataset es muy grande, con una muestra obtendremos resultados muy similares y nos permitirá trabajar con más agilidad.
all_data = pd.DataFrame(X)
all_data['y'] = y
sample_data = all_data.sample(frac = 0.1, random_state=24)
X = sample_data.drop(columns=['y'])
y = sample_data['y']
print(all_data.shape)
print(sample_data.shape)
(70000, 785) (7000, 785)
Dividid el dataset en dos subconjuntos, train (80% de los datos) y test (20% de los datos). Nombrad los conjuntos como: X_train, X_test, y_train, y_test. Utilizad la opción random_state = 24.
Podéis utilizar la implementación train_test_split de sklearn.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=24)
Para poder visualizar los resultados de cada algoritmo supervisado, reduciremos el dataset anterior a dos dimensiones, tal como hicimos en la PEC2.
model = umap.UMAP(n_components=2, random_state=42)
model.fit(X_train)
X_train_projection = model.transform(X_train)
X_test_projection = model.transform(X_test)
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
for i in range(10):
ax.scatter(X_test_projection[y_test == i,0], X_test_projection[y_test == i,1], s=3, label=str(i))
plt.legend()
plt.tight_layout()
A lo largo de los ejercicios aprenderemos a ver gráficamente las fronteras de decisión que nos devuelven los diferentes modelos. Para ello utilizaremos la función definida a continuación, que sigue los siguientes pasos:
Una vez hecho esto, ya podemos hacer el gráfico de las fronteras de decisión y añadir los puntos reales. Así veremos las áreas que el modelo considera que son de una clase y las que considera que son de otra. Al poner encima los puntos veremos si los clasifica correctamente en el área que les corresponde.
# vamos a crear la meshgrid con los valores mínimos y máximos de los ejes x e y
x_min, x_max = X_test_projection[:, 0].min() - 1, X_test_projection[:, 0].max() + 1
y_min, y_max = X_test_projection[:, 1].min() - 1, X_test_projection[:, 1].max() + 1
# definimos la función que visualizará la frontera de decisión
def plot_decision_boundaries(model, X_test_projection, y_test):
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.05),
np.arange(y_min, y_max, 0.05))
# precedimos con el clasificador con los valores de la meshgrid
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
# definimos los colores (uno para cada clase)
cmap_light = ListedColormap(['gainsboro','lightgreen','peachpuff','lightcyan', 'pink',
'lightyellow','lavender','lightcoral', 'lightskyblue', 'aquamarine'])
cmap_bold = ['grey','g','sandybrown','c','palevioletred',
'y','mediumpurple','firebrick', 'dodgerblue', 'mediumaquamarine']
# dibujamos las fronteras
Z = Z.reshape(xx.shape)
plt.figure(figsize=(20,10))
plt.pcolormesh(xx, yy, Z, cmap=cmap_light)
# dibujamos los puntos
for i in range(10):
plt.scatter(X_test_projection[y_test == i,0], X_test_projection[y_test == i,1],
s=3, label=str(i), c=cmap_bold[i])
plt.legend()
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
El objetivo de este primer ejercicio es entender el funcionamiento del algoritmo Naïve-Bayes, un algoritmo peculiar ya que se basa completamente en teoría de probabilidades.
Con el dataset de train reducido a dos dimensiones, entrenad un modelo Naïve-Bayes y representad gráficamente la frontera de decisión con el de test. Podéis utilizar el clasificador GaussianNB de sklearn.
Calculad el accuracy del modelo obtenido sobre train y test y la matriz de confusión sobre test. Podéis utilizar accuracy_score y confusion_matrix del paquet metrics de sklearn.
clf_GNB = GaussianNB()
clf_GNB.fit(X_train_projection, y_train)
GaussianNB()
plot_decision_boundaries(clf_GNB, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
y_pred = clf_GNB.predict(X_test_projection)
accuracy_score(y_test, y_pred)
0.8614285714285714
confusion_matrix(y_test, y_pred)
array([[127, 0, 0, 0, 0, 0, 2, 0, 0, 0],
[ 0, 142, 6, 0, 0, 0, 0, 0, 0, 0],
[ 8, 3, 101, 0, 1, 0, 1, 5, 0, 2],
[ 0, 6, 1, 136, 1, 3, 1, 4, 2, 0],
[ 0, 2, 0, 0, 142, 0, 0, 3, 0, 9],
[ 1, 0, 0, 16, 1, 106, 1, 2, 0, 1],
[ 1, 0, 1, 0, 0, 1, 142, 0, 0, 0],
[ 0, 5, 2, 0, 1, 1, 0, 137, 0, 6],
[ 1, 8, 0, 17, 4, 3, 0, 1, 85, 1],
[ 1, 0, 1, 2, 43, 1, 0, 11, 0, 88]], dtype=int64)
Análisis del ejercicio.
Las fronteras de decisión resultantes de la aplicación del Modelo de Naïve Bayes, se encuentran rodeando aquellos grupos de puntos donde más se acumulan los puntos de una misma clase. Esto tiene sentido ya que, el algoritmo de Naïve Bayes se basa en la probabilidad, por lo que los puntos más confusos (es decir, aquellos que se encuentran en la frontera de decisión) y difíciles de clasificar por tanto para el mismo son aquellos que se encuentran más alejados del grupo que representa a la mayoría de una clase, pues la probabilidad comienza a disminuir al haber un menor número de puntos.
Las predicciones obtenidas sobre el conjunto de test son bastante satisfactorias, prueba de ello es que la accuracy obtenida es de 0.86, lo que indica que el 86% de los datos han sido clasificados correctamente. Además, podemos ver en la matriz de confusión que el modelo no se encuentra desbalanceado, es decir, que predice bien solo un conjunto de clases. El problema está sobre todo en las clases 8 y 9, debido a que tienen grupos de otras clases muy concentrados muy cerca, y esto hace que el algoritmo haga predicciones erróneas, pues empieza a aumentar la probabilidad de que pertenezca a otra clase.
El objetivo de este segundo ejercicio es entender el funcionamiento del KNN, intuir sus principales ventajas o desventajas y entender la influencia de los parámetros de los que está compuesto.
K-Nearest-Neighbor es un algoritmo basado en instancia de tipo supervisado.
Vamos a ver qué significa esto:
¿Cómo funciona KNN?
Con el dataset de train reducido a dos dimensiones, entrenad un modelo KNN con n_neighbors = 2 y representad gráficamente la frontera de decisión con el de test.
Podéis utilizar el clasificador KNeighborsClassifier de sklearn.
clf_KNN = KNeighborsClassifier(n_neighbors=2)
clf_KNN.fit(X_train_projection, y_train)
KNeighborsClassifier(n_neighbors=2)
plot_decision_boundaries(clf_KNN, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
En el modelo entrenado, hemos fijado el parámetro n_neighbors de forma arbitraria. Pero podría ser que con otro valor obtuviéramos una mejor predicción.
Para conocer el valor óptimo de los parámetros de un modelo (hyperparameter tunning) se suele utilizar una búsqueda de rejilla (grid search). Es decir, entrenar un modelo para cada combinación de hiperparámetros posible y evaluarlo utilizando validación cruzada (cross validation) con 4 particiones estratificadas. Posteriormente, se elige la combinación de hiperparàmetres que mejores resultados haya obtenido.
En este caso sólo queremos optimizar un hiperparámetro:
Cálculo del valor óptimo del hiperparámetro k (n_neighbors). Utilizad una búsqueda de rejilla con validación cruzada para encontrar el valor óptimo de k. Por cada valor, calculad su promedio y la desviación estándar. Implementad un heatmap para visualizar la precisión según los diferentes valores del hiperparámetro.
Puede utilizar el módulo GridSearchCV de sklearn el cálculo del mejor hiperparámetro, y heatmap de Seaborn.
grid_params = {'n_neighbors': list(range(1, 11))}
gs_KNN = GridSearchCV(
KNeighborsClassifier(),
grid_params,
scoring='accuracy',
n_jobs=-1,
cv=4
)
gs_KNN.fit(X_train_projection, y_train)
gs_KNN.best_estimator_
KNeighborsClassifier(n_neighbors=10)
print('Mean test score by n_neighbors')
for i, x in enumerate(gs_KNN.cv_results_['mean_test_score']):
print('n_neighbors = %2d %4.3f' % (i+1, x))
Mean test score by n_neighbors n_neighbors = 1 0.899 n_neighbors = 2 0.901 n_neighbors = 3 0.927 n_neighbors = 4 0.926 n_neighbors = 5 0.929 n_neighbors = 6 0.931 n_neighbors = 7 0.931 n_neighbors = 8 0.932 n_neighbors = 9 0.932 n_neighbors = 10 0.932
print('Std test score by n_neighbors')
for i, x in enumerate(gs_KNN.cv_results_['std_test_score']):
print('n_neighbors = %2d %4.3f' % (i+1, x))
Std test score by n_neighbors n_neighbors = 1 0.009 n_neighbors = 2 0.007 n_neighbors = 3 0.006 n_neighbors = 4 0.008 n_neighbors = 5 0.007 n_neighbors = 6 0.006 n_neighbors = 7 0.005 n_neighbors = 8 0.006 n_neighbors = 9 0.005 n_neighbors = 10 0.006
pvt = pd.pivot_table(pd.DataFrame(gs_KNN.cv_results_),
values='mean_test_score', columns='param_n_neighbors')
sns.heatmap(pvt)
<AxesSubplot:xlabel='param_n_neighbors'>
Con el mejor hiperparámetro encontrado, volved a entrenar un clasificador KNN (con train) y representar las fronteras de decisión con los puntos de test.
Calcular el accuracy del modelo obtenido sobre test y la matriz de confusión. Podéis utilizar accuracy_score y confusion_matrix de metrics de sklearn.
gs_KNN.best_estimator_.n_neighbors
10
clf_KNN = KNeighborsClassifier(n_neighbors=gs_KNN.best_estimator_.n_neighbors)
clf_KNN.fit(X_train_projection, y_train)
KNeighborsClassifier(n_neighbors=10)
plot_decision_boundaries(clf_KNN, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
y_pred = clf_KNN.predict(X_test_projection)
accuracy_score(y_test, y_pred)
0.905
confusion_matrix(y_test, y_pred)
array([[127, 0, 0, 0, 0, 0, 2, 0, 0, 0],
[ 0, 148, 0, 0, 0, 0, 0, 0, 0, 0],
[ 8, 3, 101, 0, 1, 0, 1, 5, 0, 2],
[ 0, 6, 1, 132, 0, 6, 1, 4, 3, 1],
[ 0, 2, 0, 0, 136, 0, 0, 1, 0, 17],
[ 1, 0, 0, 6, 1, 116, 1, 2, 0, 1],
[ 1, 0, 1, 0, 0, 1, 142, 0, 0, 0],
[ 0, 6, 1, 0, 1, 1, 0, 135, 0, 8],
[ 1, 8, 0, 7, 3, 3, 0, 1, 95, 2],
[ 1, 0, 1, 2, 3, 1, 0, 4, 0, 135]], dtype=int64)
Análisis del ejercicio:
n_neighbors? ¿Tiene sentido esta diferencia entre los dos gráficos al cambiar el parámetro?Al realizar la búsqueda del mejor hiperparámetro, en este caso n_neighbors o el número de vecinos que utiliza el algoritmo para realizar la clasificación, vemos que la precisión aumenta a medida que este también aumenta, esto es debido aque en el conjunto de datos utilizado la mayoría de los puntos que pertenecen a una misma clase se encuentran agrupados en conjuntos de datos compactos, pero dentro de esos grupos existen puntos que pertenecen a otras clases y que al utilizar un número pequeño de vecinos más cercanos puede hacer al algoritmo clasificar mal un punto, sin embargo al aumentar el número de vecinos más cercanos, al haber grupos tan densos, clasifica mejor porque toma el criterio de la mayoría que le rodean y no de algunos puntos sueltos que pueden pertenecer a otras clases.
Si nos fijamos en el primer gráfico de las fronteras de decisión podemos observar que el número de fronteras de decisión, es decir, donde la clasificación del dato es confusa es mucho mayor y sus formas no se adaptan tanto a los datos de una misma clase, apareciendo algunas fronteras pequeñas en grupos donde la mayoría de puntos pertenecen a una misma clase. Esto tiene sentido ya que al tener un número de n_neighbors igual a 2, con que aparezcan dos puntos que pertenecen a otra clase diferente dentro de un grupo de puntos donde la mayoría pertenecen a una clase, ya produce una confusión para el algoritmo. Sin embargo, al aumentar el número de vecinos (n_neigbors) a 10, en el segundo gráfico, vemos que las fronteras de decisión son menores y que estas se ajustan más a los grupos de los datos, esto es debido a que ya para producir una confusión al algortmo tiene que haber 10 puntos que pertenezcan a otra clase, por lo que dicha confusión ocurre en mucha menor medida que cuando el número de vecinos es dos dado el conjunto de datos que estamos utilizando, que como se ha comentado anteriormente, crea grupos muy compactos que pertencen a una misma clase.
Con respecto a la forma de las fronteras de decisión, vemos que a diferencia del modelo de Naïve Bayes, estas no son curvas si no que tienen una forma más tajante, lo cual tiene sentido ya que este algoritmo no se basa en la probabilidad sino en los n vecinos más cercanos, por lo que la frontera se encuentran sobre todo en aquellas zonas donde hay puntos de más de una clase.
Las predicciones obtenidas en el conjunto de test son muy satisfactorias, al utilizar un número de vecinos igual a 10. Como se puede ver la accuracy indica que el 90% de los datos han sido clasificados correctamente y la matriz de confusión indica que dicha clasificación se realiza correctamente en todas las clases y que el modelo por tanto no se encuentra desbalanceado.
Las Support Vector Machine se fundamentan en el Máximal Margin Classifier, que a su vez, se basan en el concepto de hiperplano.
En un espacio p-dimensional, un hiperplano se define como un subespacio plano y afín de dimensiones p-1. El término afín significa que el subespacio no debe pasar por el origen. En un espacio de dos dimensiones, el hiperplano es un subespacio de 1 dimensión, es decir, una recta. En un espacio tridimensional, un hiperplano es un subespacio de dos dimensiones, un plano convencional. Para dimensiones p>3 no es intuitivo visualizar un hiperplano, pero el concepto de subespacio con p-1 dimensiones se mantiene.
La definición de hiperplano para casos perfectamente separables linealmente resulta en un número infinito de posibles hiperplanos, lo que hace necesario un método que permita seleccionar uno de ellos como clasificador óptimo.
La solución a este problema consiste en seleccionar como clasificador óptimo al que se conoce como maximal margin hyperplane o hiperplano óptimo de separación, que se corresponde con el hiperplano que se encuentra más alejado de todas las observaciones de entrenamiento. Para obtenerlo, se debe calcular la distancia perpendicular de cada observación a un determinado hiperplano. La menor de estas distancias (conocida como margen) determina cómo de lejos está el hiperplano de las observaciones de entrenamiento. El maximal margin hyperplane se define como el hiperplano que consigue un mayor margen, es decir, que la distancia mínima entre el hiperplano y las observaciones es lo más grande posible. Aunque esta idea suena razonable, no es posible aplicarla, ya que habría infinitos hiperplanos contra los que medir las distancias. En su lugar, se recurre a métodos de optimización.
El proceso de optimización tiene la peculiaridad de que sólo las observaciones que se encuentran justo al margen o que lo violan influyen sobre el hiperplano. A estas observaciones se les conoce como vectores soporte (vectors suport) y son las que definen el clasificador obtenido.
Hay veces en que no hay manera de encontrar un hiperplano que permita separar dos clases. En estos casos decimos que las clases no son linealmente separables. Para resolver este problema podemos utilizar el truco del núcleo .
El truco del núcleo (kernel trick) consiste en utilizar una dimensión nueva en la que podamos encontrar un hiperplano para separar las clases. Se puede ver un un ejemplo en: https://www.youtube.com/watch?v=OdlNM96sHio
Al igual que en el algoritmo visto anteriormente (KNN), las SVM también dependen de varios hiperparámetros.
En este caso intentaremos optimizar dos hiperparámetros:
C: es la regularización, es decir, el valor de penalización de los errores en la clasificación. Indica el compromiso entre obtener el hiperplano con el margen más grande posible y clasificar el máximo número de ejemplos correctamente. Probaremos los valores: 0.01, 0.1, 1, 10, 50, 100 y 200.
gama: coeficiente que multiplica la distancia entre dos puntos en el kernel radial. Para decirlo a "grosso modo", cuanto más pequeño es gama, más influencia tienen dos puntos cercanos. Probaremos los valores: 0.001, 0.01, 0.1, 1 y 10.
Al igual que en el caso anterior, para validar el rendimiento del algoritmo con cada combinación de hiperparámetros utilizaremos validación cruzada (cross-validation) con 4 particiones estratificadas.
Cálcular del valor óptimo de los hiperparámetros C y gama. Utilizad una búsqueda de rejilla con validación cruzada para encontrar los valores óptimos. Para cada combinación de valores, calcular su promedio y la desviación estándar. Haced un heatmap para visualizar la precisión según los diferentes valores de los hiperparámetros.
Podéis utilizar el módulo GridSearchCV de sklearn el cálculo de los mejores hiperparámetros con el clasificador SVC (de SVM de sklearn), y heatmap de Seaborn.
grid_params = {'C': [0.01, 0.1, 1, 10, 50, 100, 200],
'gamma':[0.001, 0.01, 0.1, 1 , 10]}
gs_SVM = GridSearchCV(
SVC(),
grid_params,
scoring='accuracy',
n_jobs=-1,
cv=4
)
gs_SVM.fit(X_train_projection, y_train)
gs_SVM.best_estimator_
SVC(C=1, gamma=10)
gs_SVM.cv_results_['mean_test_score']
array([0.11035714, 0.59392857, 0.84303571, 0.91839286, 0.11 ,
0.59017857, 0.89285714, 0.91910714, 0.92857143, 0.93089286,
0.89035714, 0.91732143, 0.92732143, 0.93196429, 0.93428571,
0.91339286, 0.92267857, 0.93035714, 0.93321429, 0.93107143,
0.92 , 0.92375 , 0.93107143, 0.93375 , 0.91928571,
0.92125 , 0.92553571, 0.93178571, 0.93321429, 0.91196429,
0.92232143, 0.92714286, 0.93160714, 0.93285714, 0.90196429])
gs_SVM.cv_results_['std_test_score']
array([0.00035714, 0.00239579, 0.0054016 , 0.00668869, 0. ,
0.00184716, 0.00416497, 0.00702356, 0.00569192, 0.00495837,
0.00321429, 0.00734317, 0.00570032, 0.00576706, 0.00728431,
0.00668869, 0.00707783, 0.00530931, 0.00572544, 0.00718736,
0.00955649, 0.0057449 , 0.00598681, 0.00734317, 0.00471104,
0.00863629, 0.00611069, 0.00647798, 0.00686049, 0.00365527,
0.00759926, 0.00660473, 0.00602662, 0.00690681, 0.00619362])
pvt = pd.pivot_table(pd.DataFrame(gs_SVM.cv_results_),
values='mean_test_score', index='param_gamma', columns='param_C')
sns.heatmap(pvt)
<AxesSubplot:xlabel='param_C', ylabel='param_gamma'>
Con la mejor combinación de hiperparámetros encuentrada, entrenad un clasificador SVM (con train) y representar las fronteras de decisión con los puntos de test.
Calcular el accuracy del modelo obtenido sobre test y la matriz de confusión. Puede utilizar accuracy_score y confusion_matrix de metrics de sklearn.
clf_SVM = SVC(C=gs_SVM.best_estimator_.C, gamma=gs_SVM.best_estimator_.gamma)
clf_SVM.fit(X_train_projection, y_train)
SVC(C=1, gamma=10)
plot_decision_boundaries(clf_SVM, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
y_pred = clf_SVM.predict(X_test_projection)
accuracy_score(y_test, y_pred)
0.9092857142857143
confusion_matrix(y_test, y_pred)
array([[127, 0, 0, 0, 0, 0, 2, 0, 0, 0],
[ 0, 148, 0, 0, 0, 0, 0, 0, 0, 0],
[ 8, 3, 101, 0, 1, 0, 1, 5, 0, 2],
[ 0, 6, 1, 132, 0, 4, 1, 4, 5, 1],
[ 0, 2, 0, 0, 136, 0, 0, 0, 0, 18],
[ 0, 0, 0, 4, 1, 118, 1, 3, 0, 1],
[ 1, 0, 0, 0, 0, 1, 142, 1, 0, 0],
[ 0, 4, 1, 0, 1, 1, 0, 138, 0, 7],
[ 1, 7, 0, 7, 3, 4, 0, 1, 95, 2],
[ 0, 0, 0, 3, 2, 0, 0, 5, 1, 136]], dtype=int64)
Análisis del ejercicio.
Los resultados obtenidos de la búsqueda de mejores valores para los hiperparámetros C y Gamma, han sido 1 y 10 respectivamente. Un mayor valor de C penaliza menos los errores obtenidos, por lo que valores muy pequeños como 0.01 o 0.1 hace que el margen de error cometido sea muy pequeño, lo que conlleva al modelo SVM a sobre ajustarse por tanto a los datos de entrenamiento y cuando llegan nuevos datos a predecir no los consigue clasificar correctamente, por lo que para este conjunto de datos concreto donde los grupos de una misma clase son normalmente densos y hay algunos casos de otras clases, es preferible utilizar un margen de error un poco mayor y clasificar bien los nuevos datos, que utilizar un margen de error pequeño y que el modelo se vea influenciado por aquellos puntos que pertenecen a otras clases pero se encuentran gráficamente en un punto donde la mayoría de puntos son de una misma clase. Del mismo modo ocurre con Gamma, que tiene el mejor valor en 10 ya que hace que la influencia que tienen dos puntos cercanos sea menor, lo qe hace que cuando existan puntos de otra clase en grupos mayoritarios de una clase, no influya mucho en el modelo.
Las fronteras de decisión son curvas y se encuentran muy cercanas a los grupos de puntos lo cual tiene sentido ya que SVM trata de maximizar la distancia entre los distintos grupos.
Las predicciones obtenidas sobre el conjunto de test son muy satisfactorias, pues se puede ver como el modelo logra clasificar correctamente el 90% del conjunto de prueba y que esta clasificación la consigue hacer correctamente en todas las clases y no sólo en algunas.
Los árboles de decisión son modelos predictivos formados por reglas binarias (si / no) con las que se consigue repartir las observaciones en función de sus atributos y predecir así el valor de la variable respuesta.
Los árboles pueden ser clasificadores (para clasificar clases, tales como nuestro ejemplo), o bien regresores (para predecir variables continuas).
La creación de las ramificaciones de los árboles se logra mediante el algoritmo de recursive binary splitting. Este algoritmo consta de tres pasos principales:
El proceso de construcción de árboles descrito tiende a reducir rápidamente el error de entrenamiento, por lo que generalmente el modelo se ajusta muy bien a las observaciones utilizadas como entrenamiento (conjunto de train). Como consecuencia, los árboles de decisión tienden al overfitting.
Para prevenirlo, utilizaremos dos hiperparámetros:
max_depth: la profundidad máxima del árbol. Exploraremos los valores entre 4 y 10.min_samples_split: el número mínimo de observaciones que debe tener una hoja del árbol para poder dividir. Exploraremos los valores: 2, 10, 20, 50 y 100.Calculad el valor óptimo de los hiperparámetros max_depth y min_samples_split. Utilizad una búsqueda de rejilla con validación cruzada para encontrar los valores óptimos. Para cada combinación de valores, calcular su promedio y la desviación estándar. Haced un heatmap para visualizar la precisión según los diferentes valores de los hiperparámetros.
Pódeis utilizar el módulo GridSearchCV de sklearn el cálculo de los mejores hiperparámetros con el clasificador DecisionTreeClassifier (de tree de sklearn), y heatmap de Seaborn.
grid_params = {'max_depth':[4, 10],
'min_samples_split':[2, 10, 20, 50, 100]}
gs_DecisionTree = GridSearchCV(
DecisionTreeClassifier(),
grid_params,
scoring='accuracy',
n_jobs=-1,
cv=4
)
gs_DecisionTree.fit(X_train_projection, y_train)
gs_DecisionTree.best_estimator_
DecisionTreeClassifier(max_depth=10, min_samples_split=100)
gs_DecisionTree.cv_results_['mean_test_score']
array([0.82875 , 0.82875 , 0.82875 , 0.82875 , 0.82875 ,
0.90696429, 0.91178571, 0.91285714, 0.91303571, 0.9175 ])
gs_DecisionTree.cv_results_['std_test_score']
array([0.03669502, 0.03669502, 0.03669502, 0.03669502, 0.03669502,
0.00461185, 0.0053809 , 0.00726678, 0.00639625, 0.00633866])
pvt = pd.pivot_table(pd.DataFrame(gs_DecisionTree.cv_results_),
values='mean_test_score', index='param_min_samples_split', columns='param_max_depth')
sns.heatmap(pvt)
<AxesSubplot:xlabel='param_max_depth', ylabel='param_min_samples_split'>
Con la mejor combinación de hiperparámetros encontrados, entrenad un clasificador DecisionTreeClassifier (con train) y representar las fronteras de decisión con los puntos de test.
Calcular el accuracy del modelo obtenido sobre test y la matriz de confusión. Puede utilizar accuracy_score y confusion_matrix de metrics de sklearn.
gs_DecisionTree.best_estimator_.min_samples_split
100
clf_DecisionTree = DecisionTreeClassifier(max_depth=gs_DecisionTree.best_estimator_.max_depth,
min_samples_split=gs_DecisionTree.best_estimator_.min_samples_split)
clf_DecisionTree.fit(X_train_projection, y_train)
DecisionTreeClassifier(max_depth=10, min_samples_split=100)
plot_decision_boundaries(clf_DecisionTree, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
y_pred = clf_DecisionTree.predict(X_test_projection)
accuracy_score(y_test, y_pred)
0.9007142857142857
confusion_matrix(y_test, y_pred)
array([[124, 0, 1, 0, 0, 0, 3, 0, 0, 1],
[ 0, 148, 0, 0, 0, 0, 0, 0, 0, 0],
[ 8, 3, 100, 1, 0, 0, 1, 5, 0, 3],
[ 0, 6, 1, 137, 0, 1, 1, 3, 3, 2],
[ 0, 2, 1, 0, 130, 0, 0, 0, 0, 23],
[ 0, 0, 1, 8, 0, 114, 1, 3, 0, 1],
[ 1, 0, 0, 0, 0, 1, 142, 0, 1, 0],
[ 0, 4, 1, 0, 2, 0, 0, 135, 1, 9],
[ 1, 8, 0, 6, 2, 5, 0, 1, 94, 3],
[ 0, 0, 1, 2, 1, 0, 0, 5, 1, 137]], dtype=int64)
Análisis del ejercicio.
Los resultados de la búsqueda de mejores valores para los hiperparámetros min_samples_split y max_depth han sido 100 y 10 respectivamente. Podemos ver como de los posibles valores con los que hemos probado la variable min_samples_split, el mejor ha sido el valor más grande igual 100, esta variable indica el número mínimo de observaciones que debe tener una hoja de un árbol para ser dividida,tiene sentido que el valor que haya salido sea el más grande, ya que los grupos de clases presentes en los datos son generalmente grupos grandes, por lo que dividir por número muy pequeños podría hacer que el clasificador acabase dividiendo de forma incorrecta y en grupos pequeños formados por casos de distintas clases, ya que existen algunos casos de distintas clases muy juntos y por tanto con características muy similares y ello puede llegar a confundir al algoritmo. Con respecto al parámetro max_depth que indica la profundidad máxima del árbol, un valor como 4 para la cantidad de clases existentes (9) era muy pequeña, por lo que es normal que el número que haya dado mejores resultados dentro de los probados haya sido el 10.
Las fronteras de decisión tienen forma de árbol, de hecho el gráfico obtenido de las fronteras de decisión parece un tree map (https://experience.sap.com/fiori-design-web/wp-content/uploads/sites/5/2017/11/vizframe_treemap_01.png), esto tiene sentido ya que es un árbol de decisión y construye un árbol para dividir los datos en las diferentes clases.
Las predicciones obtenidas sobre el conjunto de test son muy satisfactorias, pues tal y como indica el valor de la accuracy, el modelo entrenado ha conseguido clasificar correctamente el 90% de los casos y en la matriz de confusión podemos ver que clasifica correctamente la mayoría de los casos.
En la práctica, casi nunca se utiliza un solo árbol de decisión, sino que se combinan muchos árboles para obtener mejores resultados. Hay dos maneras de combinar árboles:
Bagging: utilizar el conjunto de entrenamiento original para generar centenar o miles de conjuntos similares utilizando muestreo con reemplazo. El algoritmo random forest está basado en este concepto, la combinación de varios árboles de decisión, cada uno entrenado con una muestra diferente de los datos. La decisión final del clasificador combinado (la random forest) se toma por mayoría, dando el mismo peso a todas las decisiones parciales tomadas por los clasificadores base (los árboles).
Boosting: se combinan varios clasificadores débiles secuencialmente, y en cada uno de ellos se da más peso a los datos que han sido erróneamente clasificadas en las combinaciones anteriores, para que se concentre así en los casos más difíciles de resolver.
Ambos métodos los estudiaremos más en detalle en la próxima PEC, pero en esta haremos un vistazo a los beneficios que nos aporta utilizar combinaciones de árboles respecto un solo árbol.
Al tratarse de árboles, sigue siendo importante optimizar max_depth y min_samples_split, pero en este caso añadiremos un hiperparámetro más. Para simplificar, de los dos parámetros anteriores optimizaremos sólo max_depth:
n_estimators: número de árboles. Exploraremos los valores: 50, 100 y 200.max_depth: la profundidad máxima del árbol. Exploraremos los valores entre 8 y 12.Escoged uno de los dos algoritmos mencionados: RandomForestClassifier o GradientBoostingClassifier. Calculad el valor óptimo de los hiperparámetros n_estimators y max_depth. Utilizad una búsqueda de rejilla con validación cruzada para encontrar los valores óptimos. Para cada combinación de valores, calcular su promedio y la desviación estándar. Haced un heatmap para visualizar la precisión según los diferentes valores de los hiperparámetros.
Podéis utilizar el módulo GridSearchCV de sklearn para el cálculo de los mejores hiperparámetros con el clasificador RandomForestClassifier o GradientBoostingClassifier (de ensemble de sklearn), y heatmap de Seaborn.
Nota: al utilizar tantos árboles, el cross validation con todas las combinaciones de parámetros es más costosa que en los ejemplos anteriores, y por lo tanto tardará más en ejecutarse.
grid_params = {'max_depth':[8, 12],
'n_estimators':[50, 100, 200]}
gs_GBoostingCl = GridSearchCV(
GradientBoostingClassifier(),
grid_params,
scoring='accuracy',
n_jobs=-1,
cv=4
)
gs_GBoostingCl.fit(X_train_projection, y_train)
gs_GBoostingCl.best_estimator_
GradientBoostingClassifier(max_depth=8, n_estimators=200)
gs_GBoostingCl.cv_results_['mean_test_score']
array([0.915 , 0.91607143, 0.9175 , 0.90607143, 0.90910714,
0.91321429])
gs_GBoostingCl.cv_results_['std_test_score']
array([0.00893571, 0.01003184, 0.00878455, 0.00623723, 0.0078956 ,
0.00585758])
pvt = pd.pivot_table(pd.DataFrame(gs_GBoostingCl.cv_results_),
values='mean_test_score', index='param_n_estimators', columns='param_max_depth')
sns.heatmap(pvt)
<AxesSubplot:xlabel='param_max_depth', ylabel='param_n_estimators'>
Con la mejor combinación de hiperparámetros encontrada, entrenad un clasificador DecisionTreeClassifier (con train) y representar las fronteras de decisión con los puntos de test.
Calcular el accuracy del modelo obtenido sobre test y la matriz de confusión. Puede utilizar accuracy_score y confusion_matrix de metrics de sklearn.
clf_GBoosting = GradientBoostingClassifier(max_depth=gs_GBoostingCl.best_estimator_.max_depth,
n_estimators=gs_GBoostingCl.best_estimator_.n_estimators)
clf_GBoosting.fit(X_train_projection, y_train)
GradientBoostingClassifier(max_depth=8, n_estimators=200)
plot_decision_boundaries(clf_GBoosting, X_test_projection, y_test)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:23: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later.
y_pred = clf_GBoosting.predict(X_test_projection)
accuracy_score(y_test, y_pred)
0.9021428571428571
confusion_matrix(y_test, y_pred)
array([[123, 0, 2, 0, 0, 0, 4, 0, 0, 0],
[ 1, 146, 0, 0, 0, 0, 0, 1, 0, 0],
[ 7, 3, 102, 0, 1, 0, 1, 5, 0, 2],
[ 0, 6, 1, 132, 0, 4, 1, 4, 5, 1],
[ 0, 2, 0, 0, 139, 0, 0, 1, 0, 14],
[ 0, 0, 0, 7, 1, 115, 1, 2, 0, 2],
[ 1, 0, 0, 1, 0, 1, 142, 0, 0, 0],
[ 0, 4, 3, 0, 1, 1, 0, 137, 0, 6],
[ 1, 8, 0, 6, 2, 7, 0, 1, 92, 3],
[ 0, 0, 1, 3, 2, 1, 0, 5, 0, 135]], dtype=int64)
Análisis del ejercicio.
Tras realizar la búsqueda de los mejores hiperparámetros, obtenemos los valores 8 y 200 para las variables max_depth y n_estimators respectivamente. Con respecto a la profundidad máxima del árbol (max_depth), el resultado obtenido está muy cercano al obtenido en el apartado anterior, parece ser que el seguir dividiendo el árbol en más grupos (elegir el valor 12) hacía que las predicciones obtenidas fuesen peores, esto tiene sentido ya que puede que al seguir dividiendo el árbol en más grupos se encontrasen subgrupos de individuos de distinta clase dentro de los grupos mayoritarios de una misma clase. Con respecto al número de árboles que combinar, hemos obtenido el valor 200, este valor es el mayor de los que hemos probado, puede que al obtener ya de base un buen resultado con un árbol de decisión, la única manera de obtener unos mejores resultados sea combinando un gran número de árboles.
Al igual que en el apartado anterior del árbol de decisión, las fronteras de decisión tienen forma de árbol, sólo que ahora estas están más difusas y existen más fronteras pequeñas dentro de grupos de datos. Tiene sentido al haber combinado 200 árboles de decisión, pues cada uno al final habrá creado un árbol diferente aunque similar y al combinar todos da lugar a un árbol más difuso.
Las predicciones obtenidas sobre el conjunto de test son generalmente buenas*, pues se ha logrado predecir correctamente algo más de un 90% de los casos del conjunto de test y además el algoritmo consigue clasificar correctamente la mayoría de las clases.
La mejora obtenida de combinar un conjunto de árboles a utilizar sólo uno, ha sido muy leve, se puede observar que la accuracy es un poco mayor, pero muy poco, al combinar los árboles de decisión. También se puede observar una mejoría en los casos clasificados correctamente de la clase 5, ya que al utilizar un sólo árbol asignaba 23 casos a la última clase (9) y sin embargo al combinar varios árboles solo asigna 14, pero dicha diferencia es mínima. También cabe decir que ya el resultado obtenido al utilizar un árbol de decisión era muy bueno, por lo que mejorar este de forma notoria es complicado.
Para este ejercicio se proporciona un dataset con datos sobre diferentes clientes de un banco que quieren comprar un piso, y si el banco les ha concedido la hipoteca o no.
La información es la siguiente:
ingresos: los ingresos mensuales de la familia.gastos_comunes: pagos mensuales de luz, agua, gas, etc.pago_coche: si se está pagando cuota por uno o más coches, y los gastos mensuales en combustible, etc.gastos_otros: compra mensual en supermercado y lo necesario para vivir.ahorros: suma de ahorros dispuestos a utilizar para la compra de la casa.vivienda: precio de la vivienda que quiere comprar esta familia.estado_civil: 0-soltero, 1-casados, 2-divorciadoshijos: cantidad de hijos menores y que no trabajan.trabajo: 0-sin empleo, 1-autónomo (freelance), 2-empleado, 3-empresario, 4-pareja: autónomos, 5-pareja: empleados, 6-pareja: autónomo y asalariado, 7-pareja: empresario y autónomo , 8-pareja: empresarios los dos o empresario y empleadohipoteca: 0-No ha sido concedida, 1-Si que ha sido concedida (esta será nuestra columna de salida, para aprender)Empezamos leyendo el dataset y viendo una muestra de las primeras filas.
hipotecas = pd.read_csv("../data/hipotecas.csv")
hipotecas.head(10)
| ingresos | gastos_comunes | pago_coche | gastos_otros | ahorros | vivienda | estado_civil | hijos | trabajo | hipoteca | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 6000 | 1000 | 0 | 600 | 50000 | 400000 | 0 | 2 | 2 | 1 |
| 1 | 6745 | 944 | 123 | 429 | 43240 | 636897 | 1 | 3 | 6 | 0 |
| 2 | 6455 | 1033 | 98 | 795 | 57463 | 321779 | 2 | 1 | 8 | 1 |
| 3 | 7098 | 1278 | 15 | 254 | 54506 | 660933 | 0 | 0 | 3 | 0 |
| 4 | 6167 | 863 | 223 | 520 | 41512 | 348932 | 0 | 0 | 3 | 1 |
| 5 | 5692 | 911 | 11 | 325 | 50875 | 360863 | 1 | 4 | 5 | 1 |
| 6 | 6830 | 1298 | 345 | 309 | 46761 | 429812 | 1 | 1 | 5 | 1 |
| 7 | 6470 | 1035 | 39 | 782 | 57439 | 606291 | 0 | 0 | 1 | 0 |
| 8 | 6251 | 1250 | 209 | 571 | 50503 | 291010 | 0 | 0 | 3 | 1 |
| 9 | 6987 | 1258 | 252 | 245 | 40611 | 324098 | 2 | 1 | 7 | 1 |
Cuando se nos proporciona un dataset, antes de empezar a hacer nada, es muy importante hacer un análisis exploratorio para conocer los datos con los que trabajaremos.
Calculad las frecuencias de la variable target (hipoteca). Analizar la distribución de las otras variables con gráficos de barras las variables categóricas y con histogramas las variables numéricas.
hipotecas['hipoteca'].value_counts()
0 135 1 67 Name: hipoteca, dtype: int64
def rand_web_color_hex():
rgb = ""
for _ in "RGB":
i = random.randrange(0, 2 ** 8)
rgb += i.to_bytes(1, "big").hex()
return rgb
def generate_histogram_ploty(ds, name):
fig = px.histogram(ds, x=name, color_discrete_sequence=['#' + rand_web_color_hex()],
labels=(dict(x=name.lower())), title="Histogram of " + name.lower())
fig.show()
def generate_plots(ds):
for col in ds:
generate_histogram_ploty(ds, col)
generate_plots(hipotecas)
Una vez hecho un primer análisis, se trata de "limpiar" el dataset y adaptarlo a nuestras necesidades (en este caso, predecir si se concederá la hipoteca o no).
Comprueba si hay valores null. En caso de haberlos, elimina las filas correspondientes.
hipotecas.isnull().sum()
ingresos 0 gastos_comunes 0 pago_coche 0 gastos_otros 0 ahorros 0 vivienda 0 estado_civil 0 hijos 0 trabajo 0 hipoteca 0 dtype: int64
pago_coche + gastos_comunes + gastos_otros) en una variable llamada gastos.financiar. Para ello crea una columna llamada financiar que será el resto del precio de la vivienda con los ahorros de la familia.hipotecas['gastos'] = hipotecas.apply(lambda func: func.pago_coche + func.gastos_comunes + func.gastos_otros, axis=1)
hipotecas.head()[['pago_coche','gastos_comunes','gastos_otros','gastos']]
| pago_coche | gastos_comunes | gastos_otros | gastos | |
|---|---|---|---|---|
| 0 | 0 | 1000 | 600 | 1600 |
| 1 | 123 | 944 | 429 | 1496 |
| 2 | 98 | 1033 | 795 | 1926 |
| 3 | 15 | 1278 | 254 | 1547 |
| 4 | 223 | 863 | 520 | 1606 |
hipotecas['financiar'] = hipotecas.apply(lambda func: func.vivienda - func.ahorros, axis=1)
hipotecas = hipotecas[['ingresos', 'estado_civil', 'hijos', 'trabajo', 'gastos','financiar','hipoteca']]
hipotecas.head()
| ingresos | estado_civil | hijos | trabajo | gastos | financiar | hipoteca | |
|---|---|---|---|---|---|---|---|
| 0 | 6000 | 0 | 2 | 2 | 1600 | 350000 | 1 |
| 1 | 6745 | 1 | 3 | 6 | 1496 | 593657 | 0 |
| 2 | 6455 | 2 | 1 | 8 | 1926 | 264316 | 1 |
| 3 | 7098 | 0 | 0 | 3 | 1547 | 606427 | 0 |
| 4 | 6167 | 0 | 0 | 3 | 1606 | 307420 | 1 |
El siguiente paso sería ver la correlación entre todas las features numéricas. Esto se hace para asegurar que no hay dos variables muy relacionadas entre sí, ya que en tal caso se debería seleccionar una de las dos o combinarlas en una nueva.
Mostrad la correlación entre todas las features numéricas. Si hay dos con una correlación superior al 80%, eliminar una de las dos.
Podéis utilizar heatmap de Seaborn, para verlas en un mapa de colores.
hipotecas_numeric = hipotecas[['ingresos','gastos','financiar']]
hipotecas_corr = hipotecas_numeric.corr(method='pearson')
hipotecas_corr
| ingresos | gastos | financiar | |
|---|---|---|---|
| ingresos | 1.000000 | 0.362823 | 0.564351 |
| gastos | 0.362823 | 1.000000 | 0.105849 |
| financiar | 0.564351 | 0.105849 | 1.000000 |
sns.heatmap(hipotecas_corr)
<AxesSubplot:>
Ya tenemos las variables finales con las que trabajaremos. Vamos a observar ahora cuál es la relación de cada una de ellas con el target.
Repite los diagramas de barras y los histogramas, separando por colores la variable target (dentro del mismo gráfico, diferenciar entre hipoteca-sí y hipoteca-no).
Nota: Puedes utilizar el parámetro alpha para que se vean los dos gráficos a la vez.
for col in hipotecas:
if col != 'hipoteca':
plt.hist(hipotecas[hipotecas['hipoteca'] == 0][col], label ='hipoteca = 0', alpha=0.5)
plt.hist(hipotecas[hipotecas['hipoteca'] == 1][col], label ='hipoteca = 1', alpha=0.5)
plt.legend(loc='upper right')
plt.xlabel(col)
plt.show()
Conclusiones obtenidas:
Finalmente, sólo nos queda pasar las variables categóricas a numéricas. Observad que a pesar de que todas las variables tengan números, esto no quiere decir que sean numéricas. Por ejemplo, la variable estado_civil tiene los valores 0-1-2, que sería lo mismo que si tuviera los valores soltero-casado-divorciado. Hay otras variables donde esto no ocurre, por ejemplo el número de hijos, que a pesar de ser categórica sí que son números, ya que es ordinal. La manera de diferenciarlo es, en el caso del número de hijos, 1 hijo es menos que 2 hijos, mientras que con el estado civil no hay un orden.
Una manera de pasar las variables categóricas en numéricas es aplicando one-hot encoding. Por ejemplo, en el caso de la variable estado_civil, lo que se haría sería crear tres columnas nuevas: soltero, casado y divorciado. Estas columnas tendrían los valores 0-1, por ejemplo, en el caso de la columna soltero tendría el valor 1 cuando estado_civil = soltero, y cero en otro caso.
Aplica one-hot-encoding a las variables categóricas que lo requieran. No olvidéis eliminar las variables originales!
Podéis utilizar OneHotEncoder de sklearn.preprocessing.
hipotecas
| ingresos | estado_civil | hijos | trabajo | gastos | financiar | hipoteca | |
|---|---|---|---|---|---|---|---|
| 0 | 6000 | 0 | 2 | 2 | 1600 | 350000 | 1 |
| 1 | 6745 | 1 | 3 | 6 | 1496 | 593657 | 0 |
| 2 | 6455 | 2 | 1 | 8 | 1926 | 264316 | 1 |
| 3 | 7098 | 0 | 0 | 3 | 1547 | 606427 | 0 |
| 4 | 6167 | 0 | 0 | 3 | 1606 | 307420 | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 197 | 3831 | 0 | 0 | 2 | 1530 | 352397 | 0 |
| 198 | 3961 | 2 | 3 | 8 | 1775 | 258541 | 0 |
| 199 | 3184 | 1 | 3 | 8 | 1915 | 352460 | 0 |
| 200 | 3334 | 1 | 2 | 5 | 1888 | 356907 | 0 |
| 201 | 3988 | 0 | 0 | 4 | 1644 | 245600 | 0 |
202 rows × 7 columns
def change_estado_civil(n):
if n == 0:
return 'soltero'
elif n== 1:
return 'casados'
elif n== 2:
return 'divorciados'
hipotecas['estado_civil'].value_counts()
2 73 0 68 1 61 Name: estado_civil, dtype: int64
hipotecas['estado_civil'] = hipotecas['estado_civil'].apply(lambda func:
change_estado_civil(func))
hipotecas['estado_civil'].value_counts()
divorciados 73 soltero 68 casados 61 Name: estado_civil, dtype: int64
hipotecas['trabajo'].value_counts()
5 31 8 29 7 27 6 23 3 21 4 19 1 18 2 18 0 16 Name: trabajo, dtype: int64
def change_trabajo(n):
if n == 0:
return 'sin_empleo'
elif n == 1:
return 'autonomo'
elif n== 2:
return 'empleado'
elif n== 3:
return 'empresario'
elif n== 4:
return 'autonomos'
elif n== 5:
return 'empleados_dos'
elif n== 6:
return 'autonomo_y_asalariado'
elif n== 7:
return 'empresario_y_autonomo'
elif n== 8:
return 'empresario_y_empleado_o_empresarios'
hipotecas['trabajo'] = hipotecas['trabajo'].apply(lambda func:
change_trabajo(func))
hipotecas['trabajo'].value_counts()
empleados_dos 31 empresario_y_empleado_o_empresarios 29 empresario_y_autonomo 27 autonomo_y_asalariado 23 empresario 21 autonomos 19 empleado 18 autonomo 18 sin_empleo 16 Name: trabajo, dtype: int64
hipotecas_one_hot = pd.get_dummies(data=hipotecas, columns=['trabajo','estado_civil'])
hipotecas_one_hot.head()
| ingresos | hijos | gastos | financiar | hipoteca | trabajo_autonomo | trabajo_autonomo_y_asalariado | trabajo_autonomos | trabajo_empleado | trabajo_empleados_dos | trabajo_empresario | trabajo_empresario_y_autonomo | trabajo_empresario_y_empleado_o_empresarios | trabajo_sin_empleo | estado_civil_casados | estado_civil_divorciados | estado_civil_soltero | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 6000 | 2 | 1600 | 350000 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| 1 | 6745 | 3 | 1496 | 593657 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| 2 | 6455 | 1 | 1926 | 264316 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
| 3 | 7098 | 0 | 1547 | 606427 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
| 4 | 6167 | 0 | 1606 | 307420 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
estado_civil o tres columnas soltero, casado y divorciado si la información es la misma? Realmente no es necesario aplicar la técnica one-hot-encoding, lo que sí que es necesario es pasar las variables categóricas a una forma que el modelo de aprendizaje automático la pueda reconocer como tal y no como una variable numérica, one-hot-encoding solo es una técnica para ello, podría haberse aplicado cualquier otra como Learned Embedding (https://medium.com/spikelab/learning-embeddings-for-your-machine-learning-model-a6cb4bc6542e)
La diferencia no está con respecto a la información que transmite el dividirlo en tres variables si no en como se representa la misma para el modelo. No es lo mismo tener una variable que puede tomar varios valores numéricos que representan categorías a tener una variable por cada una de ellas que tengan un valor 0 o 1 si estas se cumplen. La diferencia está en que cuando se construye un modelo de aprendizaje automático, aquellas con un valor a 0 no influirán en el valor obtenido por el mismo, sin embargo al tenerla en una sola variable, el valor 0 supondría no influir en el mismo, y los siguientes valores influirán y cada vez más al ser valores mayores y ese no es el objetivo.
A nivel conceptual no es correcto dejar las variables tal y como las teníamos porque el modelo las hubiese interpretado como una variable numérica y realmente son variables categóricas y esto haría que el modelo asignase un peso diferente a cada categoría por el valor que las representa y no por el peso que realmente tiene que una variable pertenezca a dicha categoría.
Ahora que ya tenemos el dataset limpio y hemos hecho un análisis de las diferentes variables, podemos proceder a entrenar un modelo para predecir si se concede una hipoteca o no.
Probaremos con 2 modelos diferentes e interpretaremos los resultados.
Para empezar, separamos el dataset entre train y test.
Dividid el dataset en dos subconjuntos: train (80% de los datos) y test (20% de los datos). Nombrad los conjuntos: X_train, X_test, y_train, y_test. Utilizad la opción random_state = 24.
Podéis utilizar la implementación train_test_split de sklearn.
X = hipotecas_one_hot.drop(['hipoteca'], axis=1)
y = hipotecas[['hipoteca']]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=24)
Utilizad un árbol de decisión simple con max_depth = 5 para acotar el dataset "hipotecas" sobre el conjunto de train. Dibujad el árbol de decisión. Calculad el accuray y la matriz de confusión sobre train y sobre test.
Para dibujar el árbol, podéis guiaros con este enlace: https://towardsdatascience.com/visualizing-decision-trees-with-python-scikit-learn-graphviz-matplotlib-1c50b4aa68dc
clf_DecisionTree = DecisionTreeClassifier(max_depth=5)
clf_DecisionTree.fit(X_train, y_train)
DecisionTreeClassifier(max_depth=5)
y_train['hipoteca'].unique()
array([0, 1], dtype=int64)
tree.export_graphviz(clf_DecisionTree,
out_file="tree_1.dot",
feature_names = X_train.columns,
class_names=['No','Yes'],
filled = True)
plt.imshow(mpimg.imread('./tree_1.png'))
<matplotlib.image.AxesImage at 0x205dff6a828>
y_pred = clf_DecisionTree.predict(X_test)
accuracy_score(y_test, y_pred)
0.8048780487804879
confusion_matrix(y_test, y_pred)
array([[22, 6],
[ 2, 11]], dtype=int64)
Interpretad el árbol de decisión:
Las variables que han obtenido un mayor peso han sido los ingresos, el tipo de trabajo y el total de dinero a financiar.
La precision obtenida ha sido de 0.8 apróximadamente, lo que indica que se han clasificado correctamente el 80% de los casos del test, se puede decir por tanto que es una buena precisión. Pero no sería correcto solo valorar el resultado obtenido a través de la precisión (accuracy), sino que también hay que ver a través de la matriz de confusión si el modelo clasifica correctamente las dos clases o si por el contrario solo clasifica bien una de ellas pero esta simboliza la mayoría de los casos y por ello la accuracy es tan alta. En este caso, tal y como se puede ver en la matriz de confusión el modelo clasifica correctamente la mayoría de los casos de las dos clases.
No se ha producido overfitting, ya que los resultados obtenidos al evaluar el conjunto de datos de los test ha sido bastante bueno, si se hubiera producido overfitting los resultados obtenidos sólo serían buenos para los datos de entrenamiento y no se hubiesen obtenido buenos resultados al evaluar el conjunto de tests.
Usad Random Forest o Gradient Boosting para acotar el dataset "hipotecas". Esta vez vamos a optimizar el modelo para obtener los mejores resultados posibles. Tal y como hemos visto en el ejercicio anterior, recuerda seguir los siguientes pasos:
grid_params = {'max_depth':[8, 12],
'n_estimators':[50, 100, 200]}
gs_RndForestCl = GridSearchCV(
RandomForestClassifier(),
grid_params,
scoring='accuracy',
n_jobs=-1,
cv=4
)
gs_RndForestCl.fit(X_train, y_train)
gs_RndForestCl.best_estimator_
C:\ProgramData\Anaconda3\lib\site-packages\sklearn\model_selection\_search.py:880: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().
RandomForestClassifier(max_depth=12)
clf_RndForest = RandomForestClassifier(max_depth=gs_RndForestCl.best_estimator_.max_depth,
n_estimators=gs_RndForestCl.best_estimator_.n_estimators)
clf_RndForest.fit(X_train, y_train)
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:3: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().
RandomForestClassifier(max_depth=12)
y_pred = clf_RndForest.predict(X_test)
accuracy_score(y_test, y_pred)
0.9024390243902439
confusion_matrix(y_test, y_pred)
array([[27, 1],
[ 3, 10]], dtype=int64)
Un Random Forest / Gradient Boosting no es tan fácil de interpretar como un simple Decision Tree. No podemos dibujar el árbol, porque son combinaciones de muchos árboles, pero si que podemos saber cuáles han sido las variables más decisivas a la hora de generar el modelo. Para saberlo, no podemos hacerlo a ojo mirando cómo se divide el árbol, sino que lo podemos consultar al modelo mediante la feature importance de este.
Muestra cada variable del modelo fitado, junto con su feature importance.
clf_RndForest.feature_importances_
array([0.37385886, 0.10256582, 0.10828126, 0.22677281, 0.00581897,
0.01019624, 0.01369746, 0.00720384, 0.02098898, 0.00986053,
0.01893628, 0.01372849, 0.02556056, 0.01724338, 0.01630183,
0.0289847 ])
plt.barh(X.columns, clf_RndForest.feature_importances_)
<BarContainer object of 16 artists>
Interpreta el clasificador:
Las variables que han tenido un mayor peso han sido los ingresos de la/s personas que solicitan la hipoteca, el total de dinero a financiar, los gastos de dicha persona/s y si la/s misma/s tiene/n hijo/s o hija/s.
La precisión obtenida ha sido de 0.9, lo que quiere decir que el clasificador ha logrado clasificar correctamente el 90% de los casos, lo cual es muy buen resultado. Además, en la matriz de confusión se puede ver que ha logrado clasificar bien las dos clases y no sólo una de ellas.
No se ha producido overfitting, ya que si no los resultados obtenidos al probar con el conjunto de test no hubieran sido tan buenos.
Hasta ahora hemos entrenado un modelo y hemos evaluado en test para hacernos una idea de la precisión de nuestro modelo con datos reales. Ahora vamos a utilizarlo.
Suponed que trabajáis en un banco y os visitan clientes que quieren una hipoteca. Aunque se ha de realizar un estudio a fondo de cada caso, así a priori utilizad el clasificador entrenado para tener una idea de si se les concederá la hipoteca o no.
data_to_predict = pd.DataFrame(columns=X_test.columns)
data_to_predict.columns
Index(['ingresos', 'hijos', 'gastos', 'financiar', 'trabajo_autonomo',
'trabajo_autonomo_y_asalariado', 'trabajo_autonomos',
'trabajo_empleado', 'trabajo_empleados_dos', 'trabajo_empresario',
'trabajo_empresario_y_autonomo',
'trabajo_empresario_y_empleado_o_empresarios', 'trabajo_sin_empleo',
'estado_civil_casados', 'estado_civil_divorciados',
'estado_civil_soltero'],
dtype='object')
case1 = pd.DataFrame([[2000, 0, 500, 200000, 0,0,0,0,0,0,0,0,0,0,0,0],], columns=X_test.columns)
case1['trabajo_empresario_y_empleado_o_empresarios'] = 1
case1['estado_civil_casados'] = 1
case2 = pd.DataFrame([[6000, 2, 3400, 320000, 0,0,0,0,0,0,0,0,0,0,0,0],], columns=X_test.columns)
case2['trabajo_autonomo_y_asalariado'] = 1
case2['estado_civil_casados'] = 1
case3 = pd.DataFrame([[9000, 1, 2250, 39000, 0,0,0,0,0,0,0,0,0,0,0,0],], columns=X_test.columns)
case3['trabajo_autonomo'] = 1
case3['estado_civil_soltero'] = 1
clf_RndForest.predict(case1)
array([0], dtype=int64)
clf_RndForest.predict_proba(case1)
array([[0.92, 0.08]])
clf_RndForest.predict(case2)
array([1], dtype=int64)
clf_RndForest.predict_proba(case2)
array([[0.245, 0.755]])
clf_RndForest.predict(case3)
array([1], dtype=int64)
clf_RndForest.predict_proba(case3)
array([[0.28, 0.72]])